# -*- encoding: UTF-8 -*- 
# @Author  : 不会取名字
# @Date    : 2023-11-25
# @Version : 0.1.0

"""
仿真系统的时间单位是分钟
"""

# In[0]:
# 导入工具包
# 数据分析工具
import numpy as np
import pandas as pd

# 数据可视化工具
import matplotlib.pyplot as plt
from pylab import mpl
# 指定默认字体：解决plot不能显示中文问题
mpl.rcParams[
    'font.sans-serif'
    ] = ['Microsoft YaHei'] 
mpl.rcParams[
    'axes.unicode_minus'
    ] = False # 解决保存图像是负号'-'显示为方块的问题
import seaborn as sns
# 随机数生成
import random 
# simpy 仿真工具包
import simpy

#In[1]:
#设置simpy仿真的各项参数

RANDOM_SEED = 42     # 随机种子
NUM_MACHINES = 2     # 洗车机器总数
WASHTIME = 5         # 洗一台车需要耗费的时间
T_INTER = 7          # 大约每隔七分钟生成一辆车
SIM_TIME = 1000      # 仿真的总时间
MIN_PATIENCE = 5     # 顾客洗车排队的最小耐心时间
MAX_PATIENCE = 15    # 顾客洗车排队的最大耐心时间

#In[2]：
#洗车模拟器
class Carwash(object):
    """ 洗车模拟器类

        项目的总类，包含如下方法：

        - __init__ 初始化类参数        
        - record_queue_length 记录仿真过程中各个时刻的队长
        - wash 汽车进入洗车器洗车
        - car 汽车进入队伍排队，进入洗车器洗车，汽车离开
        - car_source 汽车的生成
    """

    def __init__( 
            self, env, 
            num_machines, 
            washtime,
            t_inter, # 生成车的间隔时间
            sim_time
            ):
        """ 初始化各参数
            创建了几个进程，进程的子进程关系用 --> 表述：

            car_source --> car
            record_queue_length
        """
        self.env = env
        self.machine = simpy.Resource(
            env, num_machines
            )
        self.washtime = washtime
        self.t_inter = t_inter
        self.sim_time = sim_time
        self.wait_time_list = [
                [], # 储存汽车的编号
                [], # 储存汽车排队等待的时间
                []  # 储存汽车是否因为耐心耗尽而离去
            ]
        self.queue_len_list = [
                [], # 储存当下时间
                []  # 各个时间下的队列长度
            ]
        self.env.process(
            self.car_source(t_inter))
        self.env.process(
            self.record_queue_length())

    def record_queue_length(self):
        """ 
        记录队列长度
        
        """
        while True:
            self.queue_len_list[0].append(env.now)
            # 表格第一个位置用来记录当前时间
            self.queue_len_list[1].append(
                len(self.machine.queue))  
            # 表格第二个位置记录当前时刻下的队伍长度
            yield env.timeout(1/60)  
            # 每隔一定时间（1 秒）记录一次
    
    def car_source(
            self,
            t_inter
            ):
        """ 汽车的生成
        """                    
        for i in range(4):  
            # 创建4个初始车辆并将它们加入队列
            # 初始就放四个，不延迟
            self.env.process(
                self.car('Car %d' % i)
                )

        while True: 
            t = random.expovariate(
                1.0 / t_inter
                )  
            # 汽车的生成服从以 1.0 / t_inter 为均值的指数分布
            yield self.env.timeout(t)
            i += 1  
            self.env.process(self.car('Car %d' % i)) 
            # 创建新的车辆并将它们加入队列
    
    def car(self, name): 
        """ 汽车进入队伍排队，进入洗车器洗车，汽车离开
        """            
        arrive = self.env.now
        print( # 打印汽车到达队伍的时间
            '%s arrives at the carwash at %.2f.' % (
                name, arrive
                )
            )  
        with self.machine.request() as request: 
            # 排队等待洗车器的空闲
            patience = random.uniform(
                MIN_PATIENCE, 
                MAX_PATIENCE)  
            # 随机生成每一辆车等待洗车的耐心值 
            end = yield request | self.env.timeout(
                patience
                )

            # 计算排队的时间
            wait = self.env.now - arrive
            # 保存汽车的名字和排队的时间
            self.wait_time_list[0].append(name) 
            self.wait_time_list[1].append(wait)

            if request in end: 
                #进入洗车器之后
                self.wait_time_list[2].append(False)  
                # 记录汽车没有因为到达耐心值而离开
                print(
                    '%s enters the carwash at %.2f.' % (
                        name, self.env.now))  
                print(
                    '%s wait the carwash for %.2f.' % (
                        name, wait))  
                # 打印汽车进入洗车器的时间和排队等待的时间
                yield self.env.process( # 创建进程 washs
                    self.wash(name)
                    )  # 进入洗车过程  
                print(
                    '%s finish the carwash at %.2f.' % (
                        name, self.env.now))  
                # 打印完成洗车的时间
            else:  
                self.wait_time_list[2].append(True)
                print(
                    '%7.4f %s: RENEGED after waitting %6.3f' % (
                        self.env.now, name, wait)
                    )  
                # 如果最终汽车没有进入洗车机
                # 则打印其在等待一定时间后退出排队并离开

    def wash(self, car):        
        """ 汽车进入洗车机进行洗车
        """        
        yield self.env.timeout(WASHTIME)
        clean_level = random.randint(50, 99)
        # 初次生成一个随机数表示车辆第一次洗车后的干净程度
        if clean_level >= 80:
            # 如果第一次干净程度大于 80% 则洗车完成
            # 若小于 80% 则再洗一次 
            print(
                "Carwash removed %d%% of %s's dirt." % (
                    clean_level, car))
        else:
            yield self.env.timeout(WASHTIME)
            clean_level = random.randint(80, 99)  
            # 在 80 和 99 间生成一个随机数表示第二次洗车的干净程度
            print(
                "Carwash removed %d%% of %s's dirt." % (
                    clean_level, car))
                        
    def result(self): 
        """ 定义一个输出结果的方法

            返回值：

            - queue_info 类，也就是“排队结果信息类”，在下面定义
        """        
        wait_time_df = pd.DataFrame([])
        wait_time_df['car'] = self.wait_time_list[0]
        wait_time_df['wait_time'] = self.wait_time_list[1]
        wait_time_df['is_reneged'] = self.wait_time_list[2]        
        queue_len_df = pd.DataFrame(
            np.array(self.queue_len_list).T,
            columns=['time', 'queue_len']
            )                    
        return queue_info(wait_time_df, queue_len_df)

#In[3]：
#定义排队结果信息类
class queue_info:
    """ 排队结果信息类

        包含如下的方法：

        - __init__ 初始化类的参数
        - info 返回两张表：各个顾客的等待时间以及是否 reneged，
               以及各个时刻的队长
        - print_info 打印两张表的排队信息
        - plot_info 把排队参数画成图
    """    
    def __init__(self, wait_time_df, queue_len_df):
        """ 
        类的参数初始化
        
        """        
        self.wait_time_df = wait_time_df
        self.queue_len_df = queue_len_df

    def info(self):
        """ 
        排队系统的参数信息
        
        """        
        return (
            self.wait_time_df, 
            self.queue_len_df)

    def print_info(self): 
        """ 
        打印排队信息表
        
        """        
        print(self.wait_time_df)
        print(self.queue_len_df)

    def plot_info(self, figsize=(8, 4), dpi=100):
        """ 画图

            该方法将会绘制两张子图：

            1. 各个顾客的排队时间（用红色标记出 reneged）
            2. 各个时间点上的队列长度
        """
        fig, axes = plt.subplots(
                2, 1,
                figsize=figsize,
                dpi=dpi)
        colors = []
        for x in self.wait_time_df['is_reneged']:
            if x:
                colors.append('red')
            else:
                colors.append('C0')
        axes[0].bar(
            self.wait_time_df['car'],
            self.wait_time_df['wait_time'],
            color=colors)
        axes[0].set_title('wait time')
        axes[0].set_xticklabels([])
        axes[1].plot(
            self.queue_len_df['time'],
            self.queue_len_df['queue_len']
            )
        axes[1].set_title('queue length')
        fig.suptitle('Queue Result')
        fig.tight_layout()
        return fig, axes

# In[4]:
# Setup and start the simulation
print('Carwash')
random.seed(RANDOM_SEED)  
# This helps reproducing the results

# In[5]:
# Create an environment and start the setup process
env = simpy.Environment()
simCarwash = Carwash(
    env, 
    NUM_MACHINES,
    WASHTIME,
    T_INTER,
    SIM_TIME
    )

# In[6]:
# Execute!
env.run(until=SIM_TIME)

# In[7]:
result = simCarwash.result()
unreneged_car = result.wait_time_df[
    result.wait_time_df['is_reneged'] == False
] # 提取没有中途跑路的汽车
all_car = result.wait_time_df

# In[9]:
result.plot_info(dpi=300)

# In[10]:
# 绘制统计分布图
fig, ax = plt.subplots(dpi=300)
sns.histplot(
    all_car['wait_time'], 
    bins=10,
    kde=True,
    ax=ax, 
    label='waiting time (unreneged)' )
ax.legend()
ax.set_xlabel('等待时间')
ax.set_ylabel('汽车数量')
ax.set_xlim(0, MAX_PATIENCE)
fig.suptitle('中途未退出汽车的等待时间分布')
